Document Layout

This document summarizes the research done on financial factors influencing Brent oil price. The research has covered three major areas:

  1. An accuracy assessment of unadjusted futures curves in predicting spot price, and the accompanying presentation for Jim Burkhard
  2. A preliminary study of risk adjusted futures curves
  3. An investigation of the relationship of tweets to trading positions and subsequently to Brent oil price

You can feel free to click on the table of contents at the left to scroll to the section of your interest.

1 Accuracy of Futures Curves, Unadjusted

1.1 Introduction

There has been much investigation over whether or not futures curves can accurately predict spot prices. In general, experts conclude that due to a varying number of factors, futures curves are generally not a good forecasting tool. The reasons are many, but the most relevant for Brent are as follows:

  1. The effect of John Maynard Keynes’ ‘normal backwardation’ theory — which suggests forward prices will always tend to be discounted to the market’s expected future spot price to give investors an incentive to take on risk from producer hedgers. Therefore, even if the price is estimated correctly, the traded price will tend to under-state the market’s real price forecast.

  2. Risk-adjusted premium, which is added in by traders to reflect the added risk of trading futures at any given time. This premium is a complex calculation, often subjective, but involving such factors as consumer sentiment, interest rates, and general economic health.

  3. The curve fails to account for the ‘real’ inflation-adjusted value.

However, in our past analysis, we discovered that compared to the EIA and internal IHS forecasts, futures curves outperform both consistently, up to the 12-18 month horizon from which we performed our analysis. In this analysis, we will dive deeper into the performance characteristics of Brent forward curves and examine accuracies on different forecast horizons and on differing points of the curve. This analysis is meant to help us gain a basic understanding of how the accuracy of futures curves may fluctuate over time, and sets us up nicely to analyze the curves on a risk-premium adjusted basis, which should theoretically yield higher accuracies.

Note: Accuracy assessments are split up into two sections, representing two types of futures curves. One is a monthly aggregate curve and the other assumes the last day of the month is the “most accurate” futures curve for the month and therefore is representative of the entire month

1.2 A First Graph

It may be fruitful to preface our analysis with a nice visualization generated by Timera Energy - the progression of oil futures throughout the past decade. From this visualization, we can get a high level idea of the general trend of futures curves and their movement tendencies.

Progression of futures curves throughout the past decade

1.3 Data Import and Cleaning

We start with the easy part, importing and cleaning the oil prices on a monthly and daily frequency. We receive monthly oil price from IHS and daily from Quandl.

#Part II: import monthly prices

oilPrice <- as.data.frame(AACloudTools::SqlToDf("SELECT * FROM eaa_prod.eaa_data WHERE name = 'SPBRENTaUK.M'"))

oilPrice$date <- substr(oilPrice$date, 1, 7)

oilPrice$name <- NULL

colnames(oilPrice) <- c('enddate', 'actualatenddate')

head(oilPrice)
##   enddate actualatenddate
## 1 1992-06         21.1490
## 2 2013-01        112.0824
## 3 2015-05         65.7880
## 4 2008-11         52.5010
## 5 2001-11         18.8880
## 6 2006-12         62.3850
#import daily prices 

oilPriceDaily <- Quandl("EIA/PET_RBRTE_D")

head(oilPriceDaily, 20)
##          Date Value
## 1  2017-07-24 47.81
## 2  2017-07-21 47.47
## 3  2017-07-20 48.54
## 4  2017-07-19 48.34
## 5  2017-07-18 47.92
## 6  2017-07-17 47.66
## 7  2017-07-14 47.89
## 8  2017-07-13 47.65
## 9  2017-07-12 46.73
## 10 2017-07-11 46.68
## 11 2017-07-10 46.57
## 12 2017-07-07 46.47
## 13 2017-07-06 48.53
## 14 2017-07-05 47.58
## 15 2017-07-04 49.15
## 16 2017-07-03 49.13
## 17 2017-06-30 47.08
## 18 2017-06-29 47.02
## 19 2017-06-28 46.45
## 20 2017-06-27 46.17
#fill in missing days with previous known price

allDays <- seq(min(oilPriceDaily$Date), max(oilPriceDaily$Date), 1)

allDays <- as.data.frame(allDays[!(allDays %in% oilPriceDaily$Date)])

oilPriceDaily <- rbind.fill(oilPriceDaily, allDays)

for(i in 1:nrow(oilPriceDaily)) {
  
if(is.na(oilPriceDaily$Date[i]) == TRUE) {
  
  oilPriceDaily$Date[i] <- oilPriceDaily$`allDays[!(allDays %in% oilPriceDaily$Date)]`[i]
        
}
}

oilPriceDaily <- oilPriceDaily[order(oilPriceDaily$Date),]

oilPriceDaily$Value <- na.locf(oilPriceDaily$Value)

oilPriceDaily$`allDays[!(allDays %in% oilPriceDaily$Date)]` <- NULL

head(oilPriceDaily)
##            Date Value
## 7666 1987-05-20 18.63
## 7665 1987-05-21 18.45
## 7664 1987-05-22 18.55
## 7667 1987-05-23 18.55
## 7668 1987-05-24 18.55
## 7663 1987-05-25 18.60

The next stages of our study will require us to split the analysis into two sections, monthly-aggregate and daily-level analysis. In the monthly-aggregate level analysis, we aggregate all futures curves for the month and take their average. The reason we aggregate for the month is because prices are on a monthly level, so it would benefit us to analyze the futures curves on a monthly level as well. The methodology we used to aggregate was as follows:

Take each vignette in a month, and for the same expiration date, average their expiration prices together.

Ex. For a vignette of July of 2010, expiration date August of 2010, we averaged together the expiration price of August of 2010 for each day in July of 2010. So, we took the mean of the expiration price for August 2010 for July 1st, 2nd, 3rd…31st. There is only one expiration price for each month.

1.4 Monthly-Aggregate Level Analysis

We first need to import and clean the Morningstar data.

#look at morningstar data

ForwardCurves <- read.csv('BrentCrude2000-2009.csv')

head(ForwardCurves)
##   INSTRUMENT_ID                 SYMBOL_DESCRIPTION EXPIRATION_DATE
## 1      1tBRNG00 Feb 00 ICE Brent Crude Oil Futures     14-Jan-2000
## 2      1tBRNH00 Mar 00 ICE Brent Crude Oil Futures     14-Feb-2000
## 3      1tBRNJ00 Apr 00 ICE Brent Crude Oil Futures     16-Mar-2000
## 4      1tBRNK00 May 00 ICE Brent Crude Oil Futures     13-Apr-2000
## 5      1tBRNM00 Jun 00 ICE Brent Crude Oil Futures     16-May-2000
## 6      1tBRNN00 Jul 00 ICE Brent Crude Oil Futures     15-Jun-2000
##   TRADE_DATETIME  OPEN  HIGH   LOW CLOSE VOLUME OPEN_INTEREST
## 1    04-Jan-2000 23.90 24.70 23.89 24.39  32509         55140
## 2    04-Jan-2000 23.15 23.90 23.15 23.69  14208         58121
## 3    04-Jan-2000 22.73 23.00 22.40 22.86   4303         26409
## 4    04-Jan-2000 21.95 22.21 21.74 22.07   1487         15367
## 5    04-Jan-2000 21.33 21.42 20.98 21.42    694         23273
## 6    04-Jan-2000 20.60 20.94 20.60 20.94     10         10352

As we can see there needs to be significant wrangling done to get the data into a usable format. In addition the data does not come aggregated, so we will need to aggregate the futures curve by month for the first stage for our analysis.

#clean date field

ForwardCurves$EXPIRATION_DATE <- dmy(ForwardCurves$EXPIRATION_DATE)

ForwardCurves$TRADE_DATETIME <- dmy(ForwardCurves$TRADE_DATETIME)

#aggregate by month

ForwardCurves$TRADE_YEARMON <- substr(ForwardCurves$TRADE_DATETIME, 1, 7)

ForwardCurves$EXPIRATION_YEARMON <- substr(ForwardCurves$EXPIRATION_DATE, 1, 7)

ForwardCurves$UniquePairing <- paste(ForwardCurves$EXPIRATION_YEARMON, ForwardCurves$TRADE_YEARMON)

ForwardCurvesAgg <- summaryBy(CLOSE ~ UniquePairing, data=ForwardCurves, FUN=mean)

ForwardCurvesAgg$valuationdate <- substr(ForwardCurvesAgg$UniquePairing, 9, 16)
ForwardCurvesAgg$enddate <- substr(ForwardCurvesAgg$UniquePairing, 1, 7)

ForwardCurvesAgg$UniquePairing <- NULL

ForwardCurvesAgg$actualatenddate <- vlookup(ForwardCurvesAgg$enddate, oilPrice, 2)

ForwardCurvesAgg <- dplyr::select(ForwardCurvesAgg, 2, 3, 1, 4)

colnames(ForwardCurvesAgg) <- c('valuationdate', 'enddate', 'price', 'actualatenddate')

morningstar <- ForwardCurvesAgg

head(morningstar)
##   valuationdate enddate    price actualatenddate
## 1       2000-01 2000-01 24.27111          25.514
## 2       2000-01 2000-02 24.98600          27.760
## 3       2000-02 2000-02 27.02800          27.760
## 4       2000-01 2000-03 24.20300          27.285
## 5       2000-02 2000-03 26.47667          27.285
## 6       2000-03 2000-03 29.00000          27.285

The morningstar data, detailing Brent forward prices prior to 2009, has now been adequately cleaned and readied for the next stage of our analysis. We now import the remainder of the futures data, from Totem

#import totem futures

futures <- AACloudTools::SqlToDf("select valuationdate, enddate, name, totemtype, price, compositeprice
from eaa_prod.totem
where
name in ('BRENT','BRENT (BULLET)') and
totemgroup = 'Crude Oil'
and period = 'Month'
                and totemtype in ('BulletSwap' , 'Swap')
order by valuationdate, enddate")

head(futures)
##   valuationdate    enddate  name totemtype price compositeprice
## 1    2009-09-22 2009-10-31 BRENT      Swap    NA       70.89818
## 2    2009-09-22 2009-11-30 BRENT      Swap    NA       71.59046
## 3    2009-09-22 2009-12-31 BRENT      Swap    NA       72.17333
## 4    2009-09-22 2010-01-31 BRENT      Swap    NA       72.83133
## 5    2009-09-22 2010-02-28 BRENT      Swap    NA       73.43000
## 6    2009-09-22 2010-03-31 BRENT      Swap    NA       73.97434

We see that this set needs significant cleaning and aggregation as well, as well as the determination of the difference between composite price and price. We need to determine if the two are interchangable, as there are many dates where only one or the other exist.

#first determine the validity of using compositeprice when there is no 'price' value

futures$diff <- (futures$price - futures$compositeprice)/futures$compositeprice
futures <- futures[complete.cases(futures), ]

#shows a mean .01% difference and 1.5% max difference over the entire time period
max(abs(futures$diff))
## [1] 0.01567301
mean(abs(futures$diff))
## [1] 0.0003120394
futures$valuationdate <- lubridate::as_date(futures$valuationdate)
futures$enddate <- as_date(futures$enddate)

#shows difference increasing over time, but always staying below 1.5%
ggplot() + 
  geom_line(data = futures, aes(x = enddate, y = abs(diff)*100, color = "diff")) + 
  ggtitle("Percent Difference between Composite Price and Price over Time") +
  xlab('Date') +
  ylab('% Difference')

We see that there is a very small difference (.01% on average) between compositeprice and price, with a max of 1.5%. It is however very interesting that differences seem to be “chunked” by date, with steady differences occuring over a few-months time span. It may be useful to ask Joel Walker, the expert on this data, why this is.

We next investigate if there is a pattern to the differences with a heatmap.

wk <- function(x) as.numeric(format(x, "%U"))

futures$dayofweek <- lubridate::wday(as.Date(futures$valuationdate))-1
futures$month <- lubridate::month(futures$valuationdate)
futures$monthweek <- wk(futures$valuationdate) - wk(as.Date(cut(futures$valuationdate, "month"))) + 1

for(i in 1:nrow(futures)) {
  
if(futures$monthweek[i] == 6) {
  futures$monthweek[i] <- 5
}
  
}

futures$year <- lubridate::year(futures$valuationdate)

futures$month2 <- factor(month.abb[futures$month])

futures$monthf<-factor(futures$month,levels=as.character(1:12),labels=c("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"),ordered=TRUE)

futures$weekdayf<-factor(futures$dayofweek,levels=rev(1:7),labels=rev(c("Mon","Tue","Wed","Thu","Fri","Sat","Sun")),ordered=TRUE)

ggplot(futures, aes(monthweek, weekdayf, fill = abs(diff * 100))) + 
  geom_tile(colour = "white") + 
  facet_grid(year~monthf) + 
  scale_fill_gradient(low="white", high="red") +
  labs(x="Week of Month",
       y="",
       title = "Time-Series Calendar Heatmap", 
       subtitle="Differences between Composite Price and Price, Percent", 
       fill="Percent Difference")

We see that differences tend to become more pronounced around the extrema of our date range. However, the percent difference never exceeds 1.5%, and for the vast majority of the time stayed underneath 0.4%. We determine that for the purposes of evaluating accuracy, this is an acceptable spread, as we’ll see later that this is orders of magnitude smaller than the average forecast error. Next we begin our cleaning.

#refresh and clean totem futures and aggregate by month

futures <- AACloudTools::SqlToDf("select valuationdate, enddate, name, totemtype, price, compositeprice
from eaa_prod.totem
where
name in ('BRENT','BRENT (BULLET)') and
totemgroup = 'Crude Oil'
and period = 'Month'
                and totemtype in ('BulletSwap' , 'Swap')
order by valuationdate, enddate")

futures$uniquepair <- paste(futures$valuationdate, futures$enddate, sep = ':')

futures$diffperc <- (futures$price - futures$compositeprice)/futures$compositeprice

futures$price[is.na(futures$price)] <- as.numeric(futures$compositeprice[is.na(futures$price)])

futures <- dplyr::select(futures, 1,2,5,7)

futures$valuation_yearmon <- substr(futures$valuationdate, 1, 7)
futures$expiration_yearmon <- substr(futures$enddate, 1, 7)

futures$uniquepair <- paste(futures$valuation_yearmon, futures$expiration_yearmon)

head(futures)
##   valuationdate    enddate    price      uniquepair valuation_yearmon
## 1    2009-09-22 2009-10-31 70.89818 2009-09 2009-10           2009-09
## 2    2009-09-22 2009-11-30 71.59046 2009-09 2009-11           2009-09
## 3    2009-09-22 2009-12-31 72.17333 2009-09 2009-12           2009-09
## 4    2009-09-22 2010-01-31 72.83133 2009-09 2010-01           2009-09
## 5    2009-09-22 2010-02-28 73.43000 2009-09 2010-02           2009-09
## 6    2009-09-22 2010-03-31 73.97434 2009-09 2010-03           2009-09
##   expiration_yearmon
## 1            2009-10
## 2            2009-11
## 3            2009-12
## 4            2010-01
## 5            2010-02
## 6            2010-03
futuresAgg <- summaryBy(price ~ uniquepair, data = futures, FUN = mean)

futuresAgg$valuationdate <- substr(futuresAgg$uniquepair, 1, 7)
futuresAgg$enddate <- substr(futuresAgg$uniquepair, 9, 16)

futuresAgg$uniquepair <- NULL

colnames(futuresAgg) <- c('price', 'valuationdate', 'enddate')

head(futuresAgg)
##      price valuationdate enddate
## 1 67.18578       2009-09 2009-10
## 2 67.91482       2009-09 2009-11
## 3 68.54009       2009-09 2009-12
## 4 69.26619       2009-09 2010-01
## 5 69.93223       2009-09 2010-02
## 6 70.54456       2009-09 2010-03
#join morningstar, totem futures, and actual prices

futuresAllMonthly <- rbind.fill(morningstar, futuresAgg)

futuresAllMonthly$actualatenddate <- vlookup(ref = futuresAllMonthly$enddate, oilPrice, 2)

head(futuresAllMonthly)
##   valuationdate enddate    price actualatenddate
## 1       2000-01 2000-01 24.27111          25.514
## 2       2000-01 2000-02 24.98600          27.760
## 3       2000-02 2000-02 27.02800          27.760
## 4       2000-01 2000-03 24.20300          27.285
## 5       2000-02 2000-03 26.47667          27.285
## 6       2000-03 2000-03 29.00000          27.285

After the data cleaning we are finally able to begin our data analysis. We first split the data into different time horizons to see if the mean and standard deviation of accuracy changes over time.

#analyze accuracy at different horizons

futuresAllMonthly$valuationdate <- paste(futuresAllMonthly$valuationdate, '01', sep = '-')
futuresAllMonthly$enddate <- paste(futuresAllMonthly$enddate, '01', sep = '-')

futuresAllMonthly$valuationdate <- lubridate::as_date(futuresAllMonthly$valuationdate)
futuresAllMonthly$enddate <- lubridate::as_date(futuresAllMonthly$enddate)

futuresAllMonthly$daysApart <- futuresAllMonthly$enddate - futuresAllMonthly$valuationdate

futuresAllMonthly$diff <- (futuresAllMonthly$price - futuresAllMonthly$actualatenddate)/(futuresAllMonthly$actualatenddate)


#3 month horizon mean diff and std dev
futuresAllMonthly %>% 
  dplyr::filter(daysApart <= 90) %>% 
  summarize(avg = mean(diff, na.rm = TRUE), sd = sd(diff, na.rm = TRUE))
##           avg        sd
## 1 0.003495009 0.1210508
futuresAllMonthly90 <- futuresAllMonthly[futuresAllMonthly$daysApart <= 90,]

#6 month horizon mean diff and std dev
futuresAllMonthly %>% 
  dplyr::filter(daysApart <= 180 & daysApart >= 90) %>% 
  summarize(avg = mean(diff, na.rm = TRUE), sd = sd(diff, na.rm = TRUE))
##          avg        sd
## 1 0.02115074 0.2861429
futuresAllMonthly180 <- futuresAllMonthly[futuresAllMonthly$daysApart <= 180,]

#12 month horizon mean diff and std dev
futuresAllMonthly %>% 
  dplyr::filter(daysApart <= 365 & daysApart >= 180) %>% 
  summarize(avg = mean(diff, na.rm = TRUE), sd = sd(diff, na.rm = TRUE))
##          avg        sd
## 1 0.02650562 0.3891441
futuresAllMonthly365 <- futuresAllMonthly[futuresAllMonthly$daysApart <= 365,]

#24 month horizon mean diff and std dev
futuresAllMonthly %>% 
  dplyr::filter(daysApart <= 730 & daysApart >= 365) %>% 
  summarize(avg = mean(diff, na.rm = TRUE), sd = sd(diff, na.rm = TRUE))
##          avg       sd
## 1 0.07595155 0.477841
futuresAllMonthly730 <- futuresAllMonthly[futuresAllMonthly$daysApart <= 730,]

We see that both the mean and standard deviation rise when we expand our evaluation of the horizon of forecast. We next look at error over time, as restricted to a 12 month futures horizon.

futuresAllMonthly$dayBucket <- round_any(as.numeric(futuresAllMonthly$daysApart), 50)
futuresAllMonthlySave <- futuresAllMonthly
futuresAllMonthly <- mutate(futuresAllMonthly, monthApart = round(daysApart/30)) %>% filter(monthApart <= 12)

#demonstrate that as expiration dates get further into the future, error increases
ggplot(futuresAllMonthly, aes(x=monthApart, y=abs(diff * 100))) + stat_summary(fun.y="mean", geom="bar") + ggtitle('nMonthContracts vs. Error') + xlab('nMonthContracts') + ylab('Error (percent)')
## Don't know how to automatically pick scale for object of type difftime. Defaulting to continuous.
## Warning: Removed 90 rows containing non-finite values (stat_summary).

We see that the pattern of error confirms our initial claim that mean and standard deviation of error rise over time.

#demonstrate relative peaks in error (2009 and 2016 as expected)
ggplot(futuresAllMonthly, aes(x=enddate, y=abs(diff * 100))) + stat_summary(fun.y="mean", geom="bar") + ggtitle('Average Error by Date') + ylab('Percent Difference')
## Warning: Removed 90 rows containing non-finite values (stat_summary).

In addition and as expected, error is maximized over turbulent times in the oil price market (2009 and 2016).

#demonstrate over and underestimations
ggplot(futuresAllMonthly, aes(x=enddate, y=diff * 100)) + stat_summary(fun.y="mean", geom="bar") + ggtitle('Average Error by Date') + ylab('Percent Difference')
## Warning: Removed 90 rows containing non-finite values (stat_summary).

Precisely, as shown above, it is overestimations that occur during the 2009 and 2016 period, and underestimations in the 2008 period as oil prices become inflated faster than market expectations.

1.5 End of Month Level Analysis

In this next section, we only look at futures from the perspective of the last day of the month. The idea behind this is on the last day of the month, the futures curve has taken in the most information about the future and therefore should be the most accurate.

#take last day of month of totem futures

lastDays <- seq(as.Date("2001-01-01"), length=1200, by="1 month") - 1
lastDaysTwo <- seq(as.Date("2001-01-01"), length=1200, by="1 month") - 2
lastDaysThree <- seq(as.Date("2001-01-01"), length=1200, by="1 month") - 3

lastDays <- as.data.frame(lastDays)
lastDays$one <- 1

lastDaysTwo <- as.data.frame(lastDaysTwo)
lastDaysTwo$one <- 1

lastDaysThree <- as.data.frame(lastDaysThree)
lastDaysThree$one <- 1

futures$valuationdate <- as_date(futures$valuationdate)
futures$enddate <- as_date(futures$enddate)

futures$lastday <- vlookup(futures$valuationdate, lastDays, 2)
futures$lastdaytwo <- vlookup(futures$valuationdate, lastDaysTwo, 2)
futures$lastdaythree <- vlookup(futures$valuationdate, lastDaysThree, 2)

futuresDaily <- futures

futuresDaily[is.na(futuresDaily)] <- 0 
futuresDaily$EOM <- futuresDaily$lastday + futuresDaily$lastdaytwo + futuresDaily$lastdaythree

futuresDaily <- futuresDaily[futuresDaily$EOM > 0,]

head(futuresDaily)
##     valuationdate    enddate    price      uniquepair valuation_yearmon
## 334    2009-09-28 2009-10-31 65.95561 2009-09 2009-10           2009-09
## 335    2009-09-28 2009-11-30 66.69022 2009-09 2009-11           2009-09
## 336    2009-09-28 2009-12-31 67.32417 2009-09 2009-12           2009-09
## 337    2009-09-28 2010-01-31 68.06667 2009-09 2010-01           2009-09
## 338    2009-09-28 2010-02-28 68.74767 2009-09 2010-02           2009-09
## 339    2009-09-28 2010-03-31 69.37906 2009-09 2010-03           2009-09
##     expiration_yearmon lastday lastdaytwo lastdaythree EOM
## 334            2009-10       0          0            1   1
## 335            2009-11       0          0            1   1
## 336            2009-12       0          0            1   1
## 337            2010-01       0          0            1   1
## 338            2010-02       0          0            1   1
## 339            2010-03       0          0            1   1
uniqueLastDays <- as.data.frame(unique(futuresDaily$valuationdate))
uniqueLastDays$year <- lubridate::year(uniqueLastDays$`unique(futuresDaily$valuationdate)`)
uniqueLastDays$month <- lubridate::month(uniqueLastDays$`unique(futuresDaily$valuationdate)`)
uniqueLastDays$index <- paste(uniqueLastDays$year, uniqueLastDays$month)

uniqueLastDays$original <- uniqueLastDays$`unique(futuresDaily$valuationdate)`

uniqueLastDays <- arrange(uniqueLastDays, desc(original))

uniqueLastDays <- uniqueLastDays[!duplicated(uniqueLastDays$index),]

futuresDaily <- futures[futures$valuationdate %in% uniqueLastDays$original,]

futuresDaily <- dplyr::select(futuresDaily, 1:3)

futuresDaily$actualatenddate <- vlookup(ref = futuresDaily$enddate, oilPriceDaily, 2)

head(futuresDaily)
##     valuationdate    enddate    price actualatenddate
## 556    2009-09-30 2009-10-31 69.62939           74.91
## 557    2009-09-30 2009-11-30 70.34107           77.77
## 558    2009-09-30 2009-12-31 70.96136           77.91
## 559    2009-09-30 2010-01-31 71.68383           71.20
## 560    2009-09-30 2010-02-28 72.34067           76.36
## 561    2009-09-30 2010-03-31 72.93066           80.37
#join with morningstar futures

ForwardCurvesDaily <- read.csv('forwardcurvessmall2.csv')

ForwardCurvesDaily$X <- NULL

colnames(ForwardCurvesDaily) <- c('enddate', 'valuationdate', 'price')

ForwardCurvesDaily <- unfactor(ForwardCurvesDaily)
ForwardCurvesDaily$enddate <- as_date(ForwardCurvesDaily$enddate)
ForwardCurvesDaily$valuationdate <- as_date(ForwardCurvesDaily$valuationdate)

futuresAllDaily <- rbind.fill(futuresDaily, ForwardCurvesDaily)
futuresAllDaily$enddate <- as_date(futuresAllDaily$enddate)
futuresAllDaily$valuationdate <- as_date(futuresAllDaily$valuationdate)

futuresAllDaily$actualatenddate <- vlookup(ref = futuresAllDaily$enddate, table = oilPriceDaily, column = 2)

futuresAllDaily$daysApart <- futuresAllDaily$enddate - futuresAllDaily$valuationdate
futuresAllDaily$diff <- (futuresAllDaily$price - futuresAllDaily$actualatenddate)/(futuresAllDaily$actualatenddate)

head(futuresAllDaily)
##   valuationdate    enddate    price actualatenddate daysApart         diff
## 1    2009-09-30 2009-10-31 69.62939           74.91   31 days -0.070492766
## 2    2009-09-30 2009-11-30 70.34107           77.77   61 days -0.095524340
## 3    2009-09-30 2009-12-31 70.96136           77.91   92 days -0.089187977
## 4    2009-09-30 2010-01-31 71.68383           71.20  123 days  0.006795369
## 5    2009-09-30 2010-02-28 72.34067           76.36  151 days -0.052636620
## 6    2009-09-30 2010-03-31 72.93066           80.37  182 days -0.092563593

We also investigate how the mean and standard deviation moves over time and compare this to the monthly-aggregate futures.

#3 month horizon mean diff and std dev
futuresAllDaily %>% 
  dplyr::filter(daysApart <= 90) %>% 
  summarize(avg = mean(diff, na.rm = TRUE), sd = sd(diff, na.rm = TRUE))
##          avg        sd
## 1 0.02336856 0.1662141
futuresAllDaily90 <- futuresAllDaily[futuresAllDaily$daysApart <= 90,]

#6 month horizon mean diff and std dev
futuresAllDaily %>% 
  dplyr::filter(daysApart <= 180 & daysApart >= 90) %>% 
  summarize(avg = mean(diff, na.rm = TRUE), sd = sd(diff, na.rm = TRUE))
##         avg        sd
## 1 0.0546006 0.3024129
futuresAllDaily180 <- futuresAllDaily[futuresAllDaily$daysApart <= 180,]

#12 month horizon mean diff and std dev
futuresAllDaily %>% 
  dplyr::filter(daysApart <= 365 & daysApart >= 180) %>% 
  summarize(avg = mean(diff, na.rm = TRUE), sd = sd(diff, na.rm = TRUE))
##         avg        sd
## 1 0.1091415 0.4134104
futuresAllDaily365 <- futuresAllDaily[futuresAllDaily$daysApart <= 365,]

#24 month horizon mean diff and std dev
futuresAllDaily %>% 
  dplyr::filter(daysApart <= 730 & daysApart >= 365) %>% 
  summarize(avg = mean(diff, na.rm = TRUE), sd = sd(diff, na.rm = TRUE))
##         avg       sd
## 1 0.2315612 0.561135
futuresAllDaily730 <- futuresAllDaily[futuresAllDaily$daysApart <= 730,]

Unexpectedly, when we take futures from only the last day of the month, we achieve less accuracy than when we take the average of the month. This could be because the effects of increased market knowledge over the course of the month are dampened quickly over time. Instead, the “wisdom of the crowds” may be a greater force at play here, with the aggregate of data over more periods of time ultimately increasing our forecast accuracy.

1.6 Comparing Monthly-Aggregate vs. End of Month Futures

We next compare the two types of looking at futures to see how their errors progress over time. It is clear that end of month futures consistently have more error than monthly mean futures, across all time horizons.

#prepare data for graphing

futuresAllDaily$dayBucket <- round_any(as.numeric(futuresAllDaily$daysApart), 50)

#join by DayBucket

futuresAllDailyDayBucket <- dplyr::select(futuresAllDaily, 6:7)

futuresAllMonthlyDayBucket <- dplyr::select(futuresAllMonthlySave, 6:7)

futuresAllDailyDayBucket$diffAbs <- abs(futuresAllDailyDayBucket$diff)

futuresAllMonthlyDayBucket$diffAbs <- abs(futuresAllMonthlyDayBucket$diff)

futuresAllDailyDayBucket <- stats::aggregate(futuresAllDailyDayBucket, by=list(unique.values = futuresAllDailyDayBucket$dayBucket), 
                                                FUN=mean, na.rm=TRUE)

futuresAllMonthlyDayBucket <- stats::aggregate(futuresAllMonthlyDayBucket, by=list(unique.values = futuresAllMonthlyDayBucket$dayBucket), 
                                             FUN=mean, na.rm=TRUE)

colnames(futuresAllDailyDayBucket) <- c('unique.values', 'daily.diff', 'daybucket', 'daily.diff.abs')
colnames(futuresAllMonthlyDayBucket) <- c('unique.values', 'monthly.diff', 'daybucket', 'monthly.diff.abs')

futuresAllDayBucket <- futuresAllDailyDayBucket
futuresAllDayBucket$monthly.diff <- vlookup(futuresAllDayBucket$unique.values, futuresAllMonthlyDayBucket, 2)
futuresAllDayBucket$monthly.diff.abs <- vlookup(futuresAllDayBucket$unique.values, futuresAllMonthlyDayBucket, 4)

1.6.1 Daily Differences vs. Monthly-Aggregate Differences, Raw

plot_ly(futuresAllDayBucket, x = ~daybucket, y = ~daily.diff * 100, type = 'bar', name = 'Daily Differences') %>%
  add_trace(y = ~monthly.diff * 100, name = 'Monthly-Aggregate Differences') %>%
  layout(xaxis = list(title = 'Days from Vignette'), yaxis = list(title = 'Difference (%)'), barmode = 'group') 
## Warning: Ignoring 26 observations

## Warning: Ignoring 26 observations

1.6.2 Daily Differences vs. Monthly-Aggregate Differences, Absolute Difference

plot_ly(futuresAllDayBucket, x = ~daybucket, y = ~daily.diff.abs * 100, type = 'bar', name = 'Daily Differences') %>%
  add_trace(y = ~monthly.diff.abs * 100, name = 'Monthly-Aggregate Differences') %>%
  layout(xaxis = list(title = 'Days from Vignette'), yaxis = list(title = 'Difference (%)'), barmode = 'group')
## Warning: Ignoring 26 observations

## Warning: Ignoring 26 observations

1.7 Conclusion

Futures are a decent benchmark for forecasting Brent prices. We have seen that over time, they of course get worse at their predictions. These accuracies need to be evaluated from a business context on a case by case basis, certain errors may be unacceptable when implemented in a trading platform, but still of practical value when considering capital investment decisions.

1.7.1 Next Steps

Now that we have a good framework set up, we plan on assessing the accuracy of risk-adjusted futures curves. To do this, we will borrow from Kilian and Hamilton’s methods, taking into account traders’ short and long positions and adjusting the futures curves according to this ratio. We hypothesize that the errors will be lessened with this adjustment and we will be able to visualize a clear improvement in accuracy for most time buckets and time periods.

2 Accuracy of Futures Curves, Unadjusted, Presentation for Jim Burkhard

Note

The following section details the presentation given to Jim Burkhard regarding futures curves accuracy. It focuses more on the business-impact and qualitative side of the analysis. Some areas may be repeated from the previous section for the sake of continuity.

2.1 Introduction

Futures curves, when treated correctly, are important to our analysis as they are a good predictor of the direction of future crude oil price, at certain time horizons

They are generally smooth and therefore do not account for monthly price perturbations, but on the aggregate do well in predicting prices

We evaluate the accuracy of two types of futures curves compared to actual prices

  1. Conventional, non-risk adjusted futures curves

  2. Risk adjusted futures curves – adjusted for the risk premium embedded in the pricing of futures contracts (this analysis is still in progress)

2.2 Analysis Introduction

Characteristics of our data set:

The data is Brent futures obtained from the following sources by vintage:

  1. January 2000 – February 2009: from Morningstar

  2. March 2009 – December 2016: from TOTEM (legacy Markit’s product)

Spot price is from IHS Markit’s Magelan database

We have one futures curve for each month

Each futures curve has monthly expiration dates going 12 months out

A large body of academic research points to the presence of risk premia in the futures contracts. The theory proposed by Keynes (1930) states that “if producers of the physical commodity want to hedge their price risk by selling futures contracts, then the arbitrageurs who take the other side of the contract may be compensated for assuming that risk in the form of a futures price below the expected future spot price”

The magnitude and direction of the risk premia reflects the market participants’ expectations of future volatility

We will first show an accuracy assessment of non risk adjusted curves; our next steps will assess the accuracy of risk-adjusted curves

2.3 Example Futures Curves

2.4 Risk Adjusted Analysis

Risk adjusted curves theoretically produce better results than non risk-adjusted curves

Risk adjustments are necessary for the following reasons:

  1. The effect of John Maynard Keynes’ ‘normal backwardation’ theory — which suggests forward prices will always tend to be discounted to the market’s expected future spot price to give investors an incentive to take on risk from producer hedgers.

  2. Risk-adjusted premium, which is added in by traders to reflect the added risk of trading futures at any given time.

  3. The curve fails to account for the ‘real’ inflation-adjusted value.

3 Futures Curves, Risk Adjusted

3.1 Introduction

Risk premia can be estimated with two broad model categories:

  1. Behavioral model: the traders’ optimizing behavior determines the level of risk premia
  2. Empirical models based on volatility in the financial market place

Behavioral models have been implemented by Hamilton and Wu (2014) and Kilian (2016) to estimate the risk premia and adjust the crude oil futures rates. The risk premia are derived for each maturity at 3, 6, 9, and 12 months based on the traders’ optimizing behavior: these latter maximize the wealth as expressed by the returns of their asset holdings;

Solving this maximization problem generates the equation for the implied risk premium that traders need to incorporate in their contracts. The parameters of this equation are estimated using only the futures rates data.

Empirical models that account for volatility in the market (i.e. GARCH models) have been widely used in the financial literature to estimate risk premia. The underlying assumption is that volatile prices incur a risk premium. For instance, variations of these models take into account the asymmetry in the news impact on prices or differentiate between the short and the long-run volatility.

3.2 Overview of GARCH models

GARCH models treat heteroscedasticity in the residuals as a variance to be modeled. Thus, in financial applications where the dependent variable is the return on an asset or portfolio the variance of the return represents the risk level of those returns. The goal of these models is to provide a volatility measure – the standard deviation – that can be used in financial decisions concerning risk analysis. Mathematically, a GARCH in mean (GARCH-M) specification is given by the following components:

3.3 GARCH estimates with crude oil futures

Risk premia for returns on crude oil futures at maturities 3/6/12 months have been estimated with a GARCH-M model without exogenous variables.

The plots of futures against the Brent spot rates show that traders tend to set the futures rates very closely to the current spot price level:

3.4 Risk premia for crude oil futures at 3/6/12-month maturities

Risk premia increase with the volatility in the market. The risk premia are higher at shorter maturities.

3.5 Risk adjusted futures curves

The 12-month futures adjusted for the risk premium are shown below against the corresponding future spot price (t + 12 months):

The 3-month futures adjusted for the risk premium are better predictors than the 12-month futures:

3.6 Risk premia, accounting for risk free rate and macro factors

Risk premia themselves theoretically need to be adjusted by the risk-free interest rate at that time as well as other macro factors (ECB Working Papers, 01/2009). The short reason behind this is that both these factors affect the way traders quantify risk premia at that specific point in time. This allows risk premia to be negative as they surely are during certain economic conditions. This adjustment is however very theoretical and only represents one point of view. We decide not to apply these to actual prices because of the highly theoretical nature of them. We estimate the ECB’s method (as the exact mechanism is not published) and show the results below.

4 Relationship of Commitment of Traders to Twitter Sentiment

4.1 Introduction

This document investigates the relationship between the commitment of trades and Twitter sentiment. Commitment of trades data is provided by the CFTC and outlines aggregated trading activity for crude oil, including end-of-week swap positions and number of trades. Twitter sentiment is provided by Social Media Analytics, and quantifies aggregate Twitter sentiment towards crude oil in terms of standard deviations from the mean sentiment.

#import reference tables and peek at the top 10 rows and a couple columns of each

cot <- read.csv('cftc.csv')

head(cot[,c(5,10:12)], 10)
##    report_date_as_yyyy_mm_dd open_interest_all
## 1                  1/18/2011            971444
## 2                  3/15/2011            895870
## 3                  5/10/2011            966619
## 4                   7/5/2011            871267
## 5                  8/30/2011            885676
## 6                 10/25/2011           1090030
## 7                 12/20/2011            975361
## 8                  1/17/2012           1060735
## 9                  3/13/2012           1290005
## 10                 2/15/2011            901967
##    prod_merc_positions_long_all prod_merc_positions_short_all
## 1                        361798                        582167
## 2                        301101                        514494
## 3                        338066                        530971
## 4                        295304                        464687
## 5                        294062                        420495
## 6                        394855                        463035
## 7                        336372                        421635
## 8                        316128                        459117
## 9                        399201                        599717
## 10                       316512                        529619
twitterSentiment <- read.csv("output.csv")

head(twitterSentiment[,1:7], 10)
##    ticker           date raw.s raw.s.mean raw.volatility raw.score     s
## 1    CL_F 9/2/2009 15:55 0.250      0.012          0.056     4.249 0.249
## 2    CL_F 9/2/2009 16:10 0.250      0.012          0.056     4.249 0.246
## 3    CL_F 9/2/2009 16:25 0.250      0.012          0.056     4.249 0.244
## 4    CL_F 9/2/2009 16:40 0.250      0.012          0.056     4.249 0.241
## 5    CL_F 9/2/2009 16:55 0.500      0.025          0.112     4.249 0.487
## 6    CL_F 9/2/2009 17:10 0.500      0.025          0.112     4.249 0.482
## 7    CL_F 9/2/2009 17:25 0.500      0.025          0.112     4.249 0.477
## 8    CL_F 9/2/2009 17:40 0.500      0.025          0.112     4.249 0.472
## 9    CL_F 9/2/2009 17:55 0.536      0.027          0.120     4.249 0.503
## 10   CL_F 9/2/2009 18:10 0.536      0.027          0.120     4.249 0.498

4.2 Data Cleaning

The data clearly needs to be cleaned and re-aggregated as they contain different (but overlapping) time periods as well as different recording frequencies. First, we focus on the cleaning the COT data.

#create a datetime stamp
cot$datetime <- as.Date(cot$report_date_as_yyyy_mm_dd,"%m/%d/%Y")

#restrict to only crude oil data
cotDisagg <- cot[cot$sourceset == 'COTD',]
cotDisagg <- cot[cot$cftc_commodity_code == '67',]

#aggregate data from different indices and different types of crude oil together
cotDisagg <- aggregate(x = cotDisagg[,c(10:187)], 
                              by = list(unique.values = cotDisagg$datetime), FUN = sum)


#create variables necessary for clean merging and organize table
cotDisagg$datetime <- cotDisagg$unique.values

cotDisagg <- orderBy(~datetime, data = cotDisagg)

cotDisaggCleaned <- cotDisagg

cotDisaggCleaned[is.na(cotDisaggCleaned)] <- 0

#peek at cleaned dataframe
head(cotDisaggCleaned[,1:3], 10)
##    unique.values open_interest_all prod_merc_positions_long_all
## 1     2011-01-04           6775051                      1227834
## 2     2011-01-11           6985813                      1291446
## 3     2011-01-18           6919228                      1333278
## 4     2011-01-25           6996976                      1381056
## 5     2011-02-01           7274386                      1397018
## 6     2011-02-08           7286226                      1420104
## 7     2011-02-15           7370128                      1399122
## 8     2011-02-22           7231904                      1347691
## 9     2011-03-01           7524034                      1331088
## 10    2011-03-08           7752215                      1333153

After we clean the COT data, we focus our attention on the Twitter sentiment data. This requires considerable cleaning as well to be in a usable form, especially in the time domain.

#cleaning time periods and creating appropriate column names

twitterSentiment$center.date <- as.Date(twitterSentiment$center.date,"%m/%d/%Y")

tweetsAggregated <- aggregate(x = twitterSentiment, 
                              by = list(unique.values = twitterSentiment$center.date), FUN = mean)

tweetsAggregated$unique.values <- gsub(" ","",x = tweetsAggregated$unique.values)

tweetsAggregatedClean <- tweetsAggregated

tweetsAggregatedClean$unique.values <- gsub("/","-",x = tweetsAggregatedClean$unique.values)

tweetsAggregatedClean <- separate(tweetsAggregatedClean, col = unique.values, into = c("Month","Day","Year"), sep = "-")

tweetsAggregatedClean$datetime <- tweetsAggregatedClean$center.date

tweetsAggregatedClean <- orderBy(~datetime, data = tweetsAggregatedClean)

tweetsAggregatedClean <- Filter(function(x)!all(is.na(x)), tweetsAggregatedClean)

#aggregate Twitter data by week, starting 12/28/2010 (a week before COT data starts)

tweetsAggregatedClean <- tweetsAggregatedClean[tweetsAggregatedClean$datetime >= '2010-12-28',]

head(tweetsAggregatedClean[,1:6], 10)
##     Month Day Year      raw.s raw.s.mean raw.volatility
## 451  2010  12   28  0.2068750  0.6482708       1.353271
## 452  2010  12   29  0.1882708  0.6799375       1.290750
## 453  2010  12   30 -1.6101979  0.5449479       1.348292
## 454  2010  12   31 -1.4616458  0.4882812       1.401573
## 455  2011  01   01 -1.2444444  0.4181250       1.429431
## 456  2011  01   02  0.1728472  0.2353750       1.244097
## 457  2011  01   03  0.1617604  0.1567604       1.165021
## 458  2011  01   04 -0.4118542  0.1586562       1.136760
## 459  2011  01   05  1.8532083  0.3501771       1.143281
## 460  2011  01   06  1.0110104  0.3307396       1.096896
datetimes <- tweetsAggregatedClean$datetime

datetimes <- datetimes[seq(1, length(datetimes), 7)]

tweetsAggregatedCleanWeekly <- tweetsAggregatedClean[,4:(ncol(tweetsAggregatedClean)-2),]

tweetsAggregatedCleanWeekly$seven_day_index <- c(0, rep(1:(nrow(tweetsAggregatedCleanWeekly)-1)%/%7))

tweetsAggregatedCleanWeekly <- group_by(tweetsAggregatedCleanWeekly, seven_day_index) %>%
  summarise_all(.funs = mean)

#lag Twitter data to be the week before, as COT data is for the week before

colnames(tweetsAggregatedCleanWeekly) <- paste('PriorWeek', colnames(tweetsAggregatedCleanWeekly), sep = "")

a <- as.data.frame(1:nrow(tweetsAggregatedCleanWeekly))
list_names <- colnames(tweetsAggregatedCleanWeekly)

for(i in 1:ncol(tweetsAggregatedCleanWeekly)){
  
  b <- as.data.frame(lag(tweetsAggregatedCleanWeekly[[i]]))
  a <- cbind(a,b)
  
}

tweetsAggregatedCleanWeekly <- a[,2:ncol(a)]
colnames(tweetsAggregatedCleanWeekly) <- list_names

tweetsAggregatedCleanWeekly$datetime <- datetimes

#peek at cleaned data

head(tweetsAggregatedCleanWeekly[,1:3], 10)
##    PriorWeekseven_day_index PriorWeekraw.s PriorWeekraw.s.mean
## 1                        NA             NA                  NA
## 2                         0    -0.51236210           0.4530997
## 3                         1     0.19920766           0.1934942
## 4                         2     1.76738558           0.2949703
## 5                         3     1.34026395           0.7987016
## 6                         4     0.87085975           1.2461978
## 7                         5     0.03714583           0.9226920
## 8                         6     0.79669363           0.6348598
## 9                         7     0.46541626           0.3026188
## 10                        8     2.59052530           0.9814435

After cleaning both dataframes, we then proceed to join them before furthering our analysis. We also engineer a key variable, “Net Length”, as advised by subject matter expert Karim Fawaz. “Net Length” is calculated by taking the difference in the change of long positions to the change in short positions. It is effectively a representation of how bullish (high net length) or bearish (low or negative net length) the market is, and should in theory correlate best with Twitter sentiment.

#join frames by datetime

cotTweets <- join(tweetsAggregatedCleanWeekly, cotDisagg)
## Joining by: datetime
cotTweets$datetime <- NULL
cotTweets$unique.values <- NULL

cotTweets[is.na(cotTweets)] <- 0

cotTweets$datetime <- datetimes

cotTweets$NetLength <- cotTweets$change_in_m_money_long_all - cotTweets$change_in_m_money_short_all

head(cotTweets[,14:18], 10)
##    PriorWeeks.dispersion PriorWeeks.buzz open_interest_all
## 1              0.0000000       0.0000000                 0
## 2              0.5301295       0.8405456           6775051
## 3              0.7096289       1.1924469           6985813
## 4              0.5681928       1.1716501           6919228
## 5              0.6759137       1.1543689           6996976
## 6              0.6488863       1.6845132           7274386
## 7              0.6601146       1.1329375           7286226
## 8              0.5181972       1.3698959           7370128
## 9              0.6751522       1.1877652           7231904
## 10             0.5356280       1.8670030           7524034
##    prod_merc_positions_long_all prod_merc_positions_short_all
## 1                             0                             0
## 2                       1227834                       1726266
## 3                       1291446                       1793263
## 4                       1333278                       1832917
## 5                       1381056                       1864425
## 6                       1397018                       1935421
## 7                       1420104                       1970614
## 8                       1399122                       1971235
## 9                       1347691                       1926492
## 10                      1331088                       1943636

4.3 Exploratory Data Analysis

Finally, we are able to take a look at the variable importance chart and confirm our expert’s hypothesis that Net Length is the most meaningful correlate to Twitter sentiment. We use a random forest model to construct a variable importance plot.

#variable importance plot

n <- names(cotTweets[,16:ncol(cotTweets)])
f <- as.formula(paste("PriorWeeks.score ~", paste(n[!n %in% c("PriorWeeks.score", 'change_in_m_money_long_all', 'change_in_m_money_short_all')], collapse = " + ")))

cotTweetsTrain <- head(cotTweets, nrow(cotTweets)*2/3)
cotTweetsTest <- tail(cotTweets, nrow(cotTweets)-(nrow(cotTweets) * 2/3))

rf <- randomForest(f, data= cotTweetsTrain)
varImpPlot(rf, main = 'Top 10 variables in predicting sentiment score', n.var = 10)

4.4 Rough Modeling - Random Forest

We create a rough model of predicted S.score vs. actual, splitting our dataset into 2/3 training and 1/3 test. We model this using a random forest regressor and draw a plot of the two series. We find that there is no meaningful prediction available via a random forest model.

predict.rf <- predict(rf, cotTweetsTest)
cotTweetsTest$sPred <- predict.rf

cotTweetsCompare <- as.data.frame(cbind(as.character(cotTweetsTest$datetime), cotTweetsTest$PriorWeeks.score, cotTweetsTest$sPred))
colnames(cotTweetsCompare) <- c('Date','Actual','Predicted')
cotTweetsCompare <- unfactor(cotTweetsCompare)
cotTweetsCompare$Date <- as.Date(cotTweetsCompare$Date,"%Y-%m-%d")
p <- ggplot() + 
  geom_line(data = cotTweetsCompare, aes(x = Date, y = Actual, color = "Actual")) + 
 geom_line(data = cotTweetsCompare, aes(x = Date, y = Predicted, color = "Predicted")) + 
  xlab('Date') +
  ylab('S.Score') +
  ggtitle('Random Forest Prediction vs. Actual Twitter Sentiment')

p

4.5 Rough Modeling - ARIMAX

We try another modeling method which is more sensitive to the time-series and possibly seasonal nature of our data, the ARIMAX model. Because of the nature of the ARIMAX model, we cannot simply throw all the variables in. Rather, we select the key variable, NetLength, as judged by both expert opinion and quantitatively, via the variable importance plot. We however see once again that there is minimal predictive power with this modeling method as well. First, we demonstrate the prediction by itself without comparison to actuals.

#arimax model 

target <- cotTweetsTrain$PriorWeeks.score
xreg <- cotTweetsTrain$NetLength
xregfuture <- cotTweetsTest$NetLength

fit <- auto.arima(target, xreg=xreg)

arima_forecast <- forecast(fit,h=5,xreg=xregfuture)
plot(arima_forecast)

Next we compare to actuals and see clearly the lack of predictive power.

cotTweetsCompare$arima <- arima_forecast$mean

p <- ggplot() + 
  geom_line(data = cotTweetsCompare, aes(x = Date, y = Actual, color = 'Actual')) + 
  geom_line(data = cotTweetsCompare, aes(x = Date, y = arima, color = 'Predicted')) + 
  xlab('Date') +
  ylab('S.Score') + 
  ggtitle('ARIMAX Prediction vs. Actual Twitter Sentiment')

p

4.6 Conclusion

We can conclude that even though there is high variable importance for net length in predicting Twitter sentiment, it is ultimately insufficient to create accurate predictions going forward. This could be because s.score is fairly volatile, even though it has been aggregated by month. With volatile series, the difficulty of accurate prediction is very high. We can see each model trying to mimic the volatility of the original series, though they are ultimately unsuccessful as the degree and magnitude of volatility is unable to be accurately predicted.

The variable importance plot is a good guide for correlates to Twitter sentiment, and can be used in the future as a discussion point when having conversation about ways to quantify market sentiment. However, we should be careful in saying anything about “predicting” market sentiment from commitment of trades data, as our preliminary modeling efforts have shown that any sort of prediction falls short of any acceptable accuracy benchmark.

Future experimentation may involve additional exploration of modeling methods if the team sees this as a valuable avenue to go down. The contribution of these efforts to our end goal of predicting the direction of oil price may be fruitful given the initial revelation that trading positions, especially net length, are decently important correlates to Twitter sentiment. We have also shown in a seperate analysis that SMA’s S.Score is a decent tracker of oil price. Thus, the successful combination of these factors may prove useful to our final product.

5 Document Conclusion

We have evaluated numerous financial factors that may influence and predict oil price, including twitter sentiment/commitment of traders, futures curves, and adjusted futures curves. The respective conclusions on their impact are stated in each individual section, but the overall conclusion is that there are many factors which influence oil price in the financial arena. Crude oil is the most traded commodity in the world (coffee is second), resulting in an extremely complex space for analysis. The factors which we have identified and analyzed are merely the beginning of a larger project.